﻿using System;
using Tesla.Content;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Input;
using Tesla.Math;
using Tesla.Scene;
using Tesla.Scene.Extension;
using Tesla.Scene.Shape;

namespace TeslaSamples.Misc {
    /// <summary>
    /// Example for getting a very basic window and scene graph set up. The engine does a lot for you under the hood
    /// in respect to the scene graph, but this example could easily be used with just a vertex buffer/effect.
    /// </summary>
    [AppDescription("Minimal Sample", PlatformCapabilities.Direct3D10 | PlatformCapabilities.XNA4, "MinimalImage", "MinimalDesc", "MinimalSource")]
    public class MinimalApp : IApp {
        //Window that we'll use render our scene to
        private Window _window;

        //Window host is how we'll be hosting our application.
        private IWindowHost _host;

        //Renderer that provides access to the graphics device and used
        //to draw geometry
        private IRenderer _renderer;

        //Timing mechanism so we can interopolate animations between frames
        private GameTimer _timer;

        //Content manager that allows us to load content and cache it, so we
        //only load stuff like textures once
        private ContentManager _content;

        //Root node of a scene graph that we'll attach geometry that represents our scene
        private Node _rootNode;
        private bool _exiting;

        /// <summary>
        /// Creates a host to run our application in and executes the "game loop"
        /// </summary>
        public void Run(RenderSystemPlatform platform, PresentationParameters pp) {
            //Tesla's core functionality is serviced based. A service is a module that provides access
            //to concrete implementation of engine objects. Common services are the render system, windowing, and input. They are
            //managed by the static "Engine" class, which serves as the primary point in the codebase to bring together all services.
            //The only service that is actually is needed to "Initialize" the engine is a render system, all others are optional.
            //
            //Since service providers are interfaces, you effectively de-couple their implementations from other services or code
            //that relies on them. Other than allowing for code to be cleanly separated into modules, this allows for the developer to mix-and-match
            //services. For example, if you're developing a Windows application, then you can use the SWFWindowProvider, WindowsKeyboardProvider,
            //and WindowsMouseProvider (which we use below) for your windowing/input needs. Then, different render systems such as OpenGL, D3D10,
            //or XNA can be used without any modification.
            //
            //This modularity allows the engine to expand in the future, to include new implementations for new platforms or allow
            //the developer to create their own specific implementations. Additionally, new services can be created such as joystick or
            //gamepad support, either as a third-party addition or integrated directly into the engine core.


            //The render system is the central service for the graphics engine. Initializing
            //the engine will create default graphics content/objects (such as the standard render states)
            Engine.Initialize(new Tesla.Direct3D10.Graphics.D3D10RenderSystemProvider());

            //A window service provider allows us to setup native windows + the means to host them as an application.
            Engine.Services.AddService(typeof(IWindowProvider), new Tesla.Windows.Core.SWFWindowProvider());

            //Input providers should be obvious (in this case, by registering a service provider we can use the Keyboard/Mouse static classes)
            Engine.Services.AddService(typeof(IKeyboardProvider), new Tesla.Windows.Input.WindowsKeyboardProvider());
            Engine.Services.AddService(typeof(IMouseProvider), new Tesla.Windows.Input.WindowsMouseProvider());

            //Get the renderer so we don't have to keep querying the render system (there is only ever one renderer, similar
            //to how there's only ever one Device in Direct3D)
            _renderer = Engine.Services.GetService<IRenderSystemProvider>().Renderer;

            //Setup your game window. Since it's set to primary window, a form will be created. Non-primary creates
            //a control that can be added to a a windows form app (e.g. editor and such)
            _window = new Window(pp, true);
            _window.AllowUserResizing = true;
            _window.IsMouseVisible = true;
            _window.Title = "Minimal Sample";

            //Create a camera - if you resize the window, make sure you update your camera's viewport and projection!
            Camera camera = new Camera(new Viewport(0, 0, pp.BackBufferWidth, pp.BackBufferHeight));
            camera.Position = new Vector3(0, 0, 100f);
            camera.SetProjection(45.0f, 1.0f, 30000.0f);
            camera.Update();

            //Set the camera onto the renderer. This can be considered the "active" camera, which would
            //be used if you draw the scene graph (first there is a culling pass). Setting the current camera
            //also sets the viewport onto the device
            _renderer.CurrentCamera = camera;

            //To resize the camera we can add an event handler to the window. If we shrink/grow the window,
            //we need to reset the camera's viewport and projection too.
            _window.ClientSizeChanged += new EventHandler(delegate(Object sender, EventArgs args) {
                Rectangle rect = _window.ClientBounds;
                camera.Viewport = new Viewport(0, 0, rect.Width, rect.Height);
                camera.SetProjection(45.0f, 1.0f, 30000.0f);
                camera.Update();

                //Set the viewport onto the renderer - if we set the renderer.CurrentCamera property, this would be taken care of too.
                _renderer.Viewport = camera.Viewport;
            });


            //Create a content manager - this allows us to load & cache assets. Very similar to XNA's way of doing content loading.
            //Every renderer has a manager for default content (currently the shader library), for platform-specific content.
            //Content managers only have the binary/material loaders registered, and all others (image, model loaders) must be registered.
            //You can also use content managers to divide up your assets into logical groups, 
            _content = new ContentManager();

            //Load content
            LoadContent();

            //Create and start the timer we'll use for update calls
            _timer = new GameTimer();

            //Engine values are common properties such as the World-View-Projection matrices, the Camera, or the timer. You can
            //tell the material to query these values, which it will automatically every frame. The value map only needs 3 inputs:
            //
            // 1. World Matrix - Set your world matrix to it before calling properties that may require it (such as a World-View-Projection)
            // 2. Timer - Set your timer before calling timing properties (time per frame, fps, etc)
            // 3. Camera - Set your current camera before calling any camera related properties (such as World-View-Projection)
            //
            //When you set the camera to the renderer (renderer.CurrentCamera), #3 is automatically taken care of. The scenegraph takes
            //care of #1. When you setup your application, you have to set #2 yourself (as we do here). If you create your own mesh classes/scene graph,
            //be aware of #1.
            Engine.ValueMap.Timer = _timer;

            _timer.Start();

            //Create a host for the window (only for primary windows), if you're familiar with Application.Run(), our SWF host is effectively that!
            _host = Engine.Services.GetService<IWindowProvider>().CreateWindowHost(_window);
            //The "game loop", which is called when the application is idle
            _host.Idle += new Heartbeat(Loop);
            //Run the app
            _host.Run();

            //Ensure we dismiss the services and clean up the resources created, when we fall out of our loop.
            Engine.CleanUpResources();
        }

        //Loadup our content
        private void LoadContent() {
            //Create a root node
            _rootNode = new Node("Scene");

            //Simple point light
            PointLight pl = new PointLight();
            pl.Attenuate = false;
            pl.Position = new Vector3(-20, 0, 20); //Put the light in the lower-left corner of the screen for this example
            _rootNode.AddLight(pl);

            //Create a rotating box
            Box box = new Box("Mybox", 10, 10, 10);
            //Set it 10 units down the -Z-axis
            box.Translation = new Vector3(0, 0, -10);
            //Add a controller that rotates it 25 degrees per second. This is updated every time the scene graph is updated.
            box.AddController(new RotateController(Vector3.UnitX, 25));
            //Create a material for the box, make sure we clone it otherwise we reuse the cached object
            box.Material = _content.Load<Material>("LitBasicColor.tem").Clone();
            //Set a red diffuse color to the effect parameter
            box.Material.SetParameter("MatDiffuse", Color.DarkRed.ToVector3());

            //Note on materials: You can programmatically create them, or define them in material files (.tem). Material files
            //can inherit from another, and you're able to set parameters, render states, and engine values. A benefit of TEM files
            //is that because you're describing them in a plain-text file, you can actually change how your objects are rendered without
            //writing code or re-compiling your application. They also serve as templates to streamline your content creation, since a lot
            //of your geometry may use the same material, or most of the same material (e.g. a color or texture change).

            _rootNode.AddChild(box);
        }

        private void UnloadContent() {
            //We can unload all the content that is currently cached by the content manager. Although we do so when we're exiting the app,
            //this can be done when levels are changed.
            _content.Unload();
        }

        //The game loop
        private void Loop() {
            //If we aren't exiting, continue
            if(!_exiting) {
                //Advance the timer
                GameTime time = _timer.Update();

                //Check inputs
                CheckInput(time);

                //Update scene
                Update(time);

                //Draw scene
                Draw(_renderer);
                //Otherwise start the exit & cleanup resources
            } else {
                //Dismiss loaded content
                UnloadContent();

                //Notifies the application it is exiting
                _host.Exit();

                //Ensure we dismiss the services and clean up the resources they created.
                Engine.CleanUpResources();
            }
        }

        //Check input, update camera
        private void CheckInput(GameTime time) {
            //Tell the app to exit if escape is pushed.
            KeyboardState ks = Keyboard.GetKeyboardState();
            if(ks.IsKeyDown(Keys.Escape)) {
                _exiting = true;
            }
            _renderer.CurrentCamera.Update();
        }

        //Update portion of loop
        private void Update(GameTime time) {
            //Do your updating here
            _rootNode.Update(time);
        }

        //Draw portion of loop
        private void Draw(IRenderer renderer) {
            //Every window has a backbuffer associated with it called a swap chain. Before we render anything to it, we first
            //must clear it. So we can have multiple windows for only one device; you clear/draw/present to each window you
            //want your scene to be rendered upon (useful for multiple views in a tool, for instance).
            _window.SwapChain.Clear(Color.CornflowerBlue);

            //Do your rendering here
            _rootNode.OnDraw(renderer);

            //Flush any remaining geometries in our render queue, which renders them
            renderer.RenderQueue.SortRenderClear();

            //Present to the window
            _window.SwapChain.Present();
        }
    }
}
